服务模块

This commit is contained in:
‘Liammcl’
2024-12-28 15:51:26 +08:00
parent 4f0e7be806
commit d021f39b04
8 changed files with 623 additions and 895 deletions

View File

@@ -1,26 +1,47 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from "react";
import { Form, Input, InputNumber, Button, Card, Typography, Modal, message, Divider, Select } from 'antd'; import {
import { PlusOutlined, DeleteOutlined, EditOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons'; Form,
import { v4 as uuidv4 } from 'uuid'; 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 { supabase } from "@/config/supabase";
import { supabaseService } from "@/hooks/supabaseService"; import { supabaseService } from "@/hooks/supabaseService";
const { Text } = Typography; const { Text } = Typography;
const SectionList = ({ const SectionList = ({
form, form,
isView, isView,
formValues, formValues,
type, type,
currentCurrency = 'CNY' currentCurrency = "TWD",
}) => { }) => {
const [editingSectionIndex, setEditingSectionIndex] = useState(null); const [editingSectionIndex, setEditingSectionIndex] = useState(null);
const [editingSectionName, setEditingSectionName] = useState(''); const [editingSectionName, setEditingSectionName] = useState("");
const [templateModalVisible, setTemplateModalVisible] = useState(false); const [templateModalVisible, setTemplateModalVisible] = useState(false);
const [availableSections, setAvailableSections] = useState([]); const [availableSections, setAvailableSections] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [units, setUnits] = useState([]); const [units, setUnits] = useState([]);
const [loadingUnits, setLoadingUnits] = useState(false); const [loadingUnits, setLoadingUnits] = useState(false);
const CURRENCY_SYMBOLS = {
CNY: "¥",
TWD: "NT$",
USD: "$",
};
// 内部计算方法
const calculateItemAmount = (quantity, price) => { const calculateItemAmount = (quantity, price) => {
const safeQuantity = Number(quantity) || 0; const safeQuantity = Number(quantity) || 0;
const safePrice = Number(price) || 0; const safePrice = Number(price) || 0;
@@ -36,19 +57,13 @@ const SectionList = ({
}; };
const formatCurrency = (amount) => { const formatCurrency = (amount) => {
const CURRENCY_SYMBOLS = {
CNY: "¥",
TWD: "NT$",
USD: "$",
};
const safeAmount = Number(amount) || 0; const safeAmount = Number(amount) || 0;
return `${CURRENCY_SYMBOLS[currentCurrency] || ""}${safeAmount.toLocaleString("zh-CN", { return `${CURRENCY_SYMBOLS[currentCurrency] || "NT$"}${safeAmount.toLocaleString("zh-TW", {
minimumFractionDigits: 2, minimumFractionDigits: 2,
maximumFractionDigits: 2, maximumFractionDigits: 2,
})}`; })}`;
}; };
// 获取可用的小节模板
const fetchAvailableSections = async () => { const fetchAvailableSections = async () => {
try { try {
setLoading(true); setLoading(true);
@@ -56,6 +71,7 @@ const SectionList = ({
.from("resources") .from("resources")
.select("*") .select("*")
.eq("type", "sections") .eq("type", "sections")
.eq("attributes->>template_type", [type])
.order("created_at", { ascending: false }); .order("created_at", { ascending: false });
if (error) throw error; if (error) throw error;
@@ -68,195 +84,235 @@ const SectionList = ({
} }
}; };
// 处理小节名称编辑
const handleSectionNameEdit = (sectionIndex, initialValue) => { const handleSectionNameEdit = (sectionIndex, initialValue) => {
setEditingSectionIndex(sectionIndex); setEditingSectionIndex(sectionIndex);
setEditingSectionName(initialValue || ''); setEditingSectionName(initialValue || "");
}; };
const handleSectionNameSave = () => { const handleSectionNameSave = () => {
if (!editingSectionName.trim()) { if (!editingSectionName.trim()) {
message.error('请输入小节名称'); message.error("请输入小节名称");
return; return;
} }
const sections = form.getFieldValue('sections'); const sections = form.getFieldValue("sections");
const newSections = [...sections]; const newSections = [...sections];
newSections[editingSectionIndex] = { newSections[editingSectionIndex] = {
...newSections[editingSectionIndex], ...newSections[editingSectionIndex],
sectionName: editingSectionName.trim() sectionName: editingSectionName.trim(),
}; };
form.setFieldValue('sections', newSections); form.setFieldValue("sections", newSections);
setEditingSectionIndex(null); setEditingSectionIndex(null);
setEditingSectionName(''); setEditingSectionName("");
}; };
// 添加项目
const handleAddItem = (add) => { const handleAddItem = (add) => {
add({ add({
key: uuidv4(), key: uuidv4(),
name: '', name: "",
description: '', description: "",
quantity: 1, quantity: 1,
price: 0, price: 0,
unit: '' unit: "",
}); });
}; };
// 处理使用模板
const handleUseTemplate = (template, add) => { const handleUseTemplate = (template, add) => {
const newSection = { const newSection = {
key: uuidv4(), key: uuidv4(),
sectionName: template.attributes.name, sectionName: template.attributes.name,
items: (template.attributes.items || []).map(item => ({ items: (template.attributes.items || []).map((item) => ({
key: uuidv4(), key: uuidv4(),
name: item.name || '', name: item.name || "",
description: item.description || '', description: item.description || "",
price: item.price || 0, price: item.price || 0,
quantity: item.quantity || 1, quantity: item.quantity || 1,
unit: item.unit || '', unit: item.unit || "",
})), })),
}; };
add(newSection); add(newSection);
setTemplateModalVisible(false); setTemplateModalVisible(false);
message.success('套用模版成功'); message.success("套用模版成功");
}; };
// 处理创建自定义小节
const handleCreateCustom = (add, fieldsLength) => { const handleCreateCustom = (add, fieldsLength) => {
add({ add({
key: uuidv4(), key: uuidv4(),
sectionName: `服务类型 ${fieldsLength + 1}`, sectionName: `服务类型 ${fieldsLength + 1}`,
items: [{ items: [
key: uuidv4(), {
name: '', key: uuidv4(),
description: '', name: "",
quantity: 1, description: "",
price: 0, quantity: 1,
unit: '' price: 0,
}] unit: "",
},
],
}); });
setTemplateModalVisible(false); setTemplateModalVisible(false);
}; };
// 获取单位列表
const fetchUnits = async () => { const fetchUnits = async () => {
setLoadingUnits(true); setLoadingUnits(true);
try { try {
const { data: units } = await supabaseService.select('resources', { const { data: units } = await supabaseService.select("resources", {
filter: { filter: {
'type': { eq: 'units' }, type: { eq: "units" },
'attributes->>template_type': { in: `(${type},common)` } "attributes->>template_type": { in: `(${type},common)` },
}, },
order: { order: {
column: 'created_at', column: "created_at",
ascending: false ascending: false,
} },
}); });
setUnits(units || []); setUnits(units || []);
} catch (error) { } catch (error) {
message.error('获取单位列表失败'); message.error("获取单位列表失败");
console.error(error); console.error(error);
} finally { } finally {
setLoadingUnits(false); setLoadingUnits(false);
} }
}; };
// 在组件加载时获取单位列表
useEffect(() => { useEffect(() => {
fetchUnits(); fetchUnits();
}, []); }, []);
// 新增单位
const handleAddUnit = async (unitName) => { const handleAddUnit = async (unitName) => {
try { try {
const { error } = await supabase const { error } = await supabase.from("resources").insert([
.from('resources') {
.insert([{ type: "units",
type: 'units',
attributes: { attributes: {
name: unitName name: unitName,
template_type: type,
}, },
schema_version: 1 schema_version: 1,
}]); },
]);
if (error) throw error; if (error) throw error;
message.success('新增单位成功'); message.success("新增单位成功");
fetchUnits(); fetchUnits();
return true; return true;
} catch (error) { } catch (error) {
message.error('新增单位失败'); message.error("新增单位失败");
console.error(error); console.error(error);
return false; return false;
} }
}; };
// 模板选择弹窗内容
const renderTemplateModalContent = (add, fieldsLength) => ( const renderTemplateModalContent = (add, fieldsLength) => (
<div className="space-y-6"> <div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> {availableSections.length > 0 ? (
{availableSections.map(section => ( <div className="flex flex-col">
<div <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
key={section.id} {availableSections.map((section) => (
className="group relative bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-lg transition-all duration-300 cursor-pointer" <div
onClick={() => handleUseTemplate(section, add)} 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"
<div className="p-6"> onClick={() => handleUseTemplate(section, add)}
<div className="text-center mb-4"> >
<h3 className="text-lg font-medium group-hover:text-blue-500 transition-colors"> <div className="p-6">
{section.attributes.name} <div className="text-center mb-4">
</h3> <h3 className="text-lg font-medium group-hover:text-blue-500 transition-colors">
<div className="text-sm text-gray-500 mt-1"> {section.attributes.name}
{section.attributes.items?.length || 0} 个项目 </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> </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>
))}
</div>
<Divider /> <div className="flex justify-center">
<Button
<div className="flex justify-center"> type="primary"
<Button icon={<PlusOutlined />}
type="dashed" onClick={() => handleCreateCustom(add, fieldsLength)}
icon={<PlusOutlined />} className="bg-blue-600 hover:bg-blue-700 border-0 shadow-md hover:shadow-lg transition-all duration-200 h-10 px-6 rounded-lg flex items-center gap-2"
onClick={() => handleCreateCustom(add, fieldsLength)} >
className="w-1/3 border-2" <span className="font-medium">自定义模块</span>
> </Button>
自定义模块 </div>
</Button> </div>
</div> ) : (
<div className="flex flex-col items-center justify-center py-16 px-4">
<div className="w-48 h-48 mb-8">
<svg
className="w-full h-full text-gray-200"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1}
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
/>
</svg>
</div>
<h3 className="text-xl font-medium text-gray-900 mb-2">
暂无可用模板
</h3>
<p className="text-gray-500 text-center max-w-sm mb-8">
您可以选择创建一个自定义模块开始使用
</p>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => handleCreateCustom(add, fieldsLength)}
size="large"
className="shadow-md hover:shadow-lg transition-shadow"
>
创建自定义模块
</Button>
</div>
)}
</div> </div>
); );
// 修改项目小计的计算,将其封装为 memo 组件
const ItemSubtotal = React.memo(({ quantity, price, currentCurrency }) => { const ItemSubtotal = React.memo(({ quantity, price, currentCurrency }) => {
const subtotal = useMemo(() => { const subtotal = useMemo(() => {
const safeQuantity = Number(quantity) || 0; const safeQuantity = Number(quantity) || 0;
@@ -273,7 +329,6 @@ const SectionList = ({
); );
}); });
// 修改小节总计的计算,将其封装为 memo 组件
const SectionTotal = React.memo(({ items, currentCurrency }) => { const SectionTotal = React.memo(({ items, currentCurrency }) => {
const total = useMemo(() => { const total = useMemo(() => {
if (!Array.isArray(items)) return 0; if (!Array.isArray(items)) return 0;
@@ -281,7 +336,7 @@ const SectionList = ({
if (!item) return sum; if (!item) return sum;
const safeQuantity = Number(item.quantity) || 0; const safeQuantity = Number(item.quantity) || 0;
const safePrice = Number(item.price) || 0; const safePrice = Number(item.price) || 0;
return sum + (safeQuantity * safePrice); return sum + safeQuantity * safePrice;
}, 0); }, 0);
}, [items]); }, [items]);
@@ -302,11 +357,11 @@ const SectionList = ({
<Form.List name="sections"> <Form.List name="sections">
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <>
<div className="space-y-4"> <div className="space-y-4 overflow-auto">
{fields.map((field, sectionIndex) => ( {fields.map((field, sectionIndex) => (
<Card <Card
key={field.key} key={field.key}
className="shadow-sm rounded-lg" className="shadow-sm rounded-lg min-w-[1000px]"
type="inner" type="inner"
title={ title={
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -315,7 +370,9 @@ const SectionList = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
value={editingSectionName} value={editingSectionName}
onChange={(e) => setEditingSectionName(e.target.value)} onChange={(e) =>
setEditingSectionName(e.target.value)
}
onPressEnter={handleSectionNameSave} onPressEnter={handleSectionNameSave}
autoFocus autoFocus
className="w-48" className="w-48"
@@ -331,7 +388,7 @@ const SectionList = ({
icon={<CloseOutlined />} icon={<CloseOutlined />}
onClick={() => { onClick={() => {
setEditingSectionIndex(null); setEditingSectionIndex(null);
setEditingSectionName(''); setEditingSectionName("");
}} }}
className="text-red-500" className="text-red-500"
/> />
@@ -340,17 +397,26 @@ const SectionList = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="w-1 h-4 bg-purple-500 rounded-full" /> <span className="w-1 h-4 bg-purple-500 rounded-full" />
<Text strong className="text-lg"> <Text strong className="text-lg">
{form.getFieldValue(['sections', sectionIndex, 'sectionName']) {form.getFieldValue([
|| `服务类型 ${sectionIndex + 1}`} "sections",
sectionIndex,
"sectionName",
]) || `服务类型 ${sectionIndex + 1}`}
</Text> </Text>
{!isView && ( {!isView && (
<Button <Button
type="link" type="link"
icon={<EditOutlined />} icon={<EditOutlined />}
onClick={() => handleSectionNameEdit( onClick={() =>
sectionIndex, handleSectionNameEdit(
form.getFieldValue(['sections', sectionIndex, 'sectionName']) sectionIndex,
)} form.getFieldValue([
"sections",
sectionIndex,
"sectionName",
])
)
}
className="text-gray-400 hover:text-blue-500" className="text-gray-400 hover:text-blue-500"
/> />
)} )}
@@ -368,11 +434,9 @@ const SectionList = ({
</div> </div>
} }
> >
{/* 项目列表 */}
<Form.List name={[field.name, "items"]}> <Form.List name={[field.name, "items"]}>
{(itemFields, { add: addItem, remove: removeItem }) => ( {(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 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>描述/备注</div>
@@ -383,7 +447,6 @@ const SectionList = ({
<div></div> <div></div>
</div> </div>
{/* 项目列表 */}
{itemFields.map((itemField, itemIndex) => ( {itemFields.map((itemField, itemIndex) => (
<div <div
key={itemField.key} key={itemField.key}
@@ -413,9 +476,10 @@ const SectionList = ({
loading={loadingUnits} loading={loadingUnits}
showSearch showSearch
allowClear allowClear
options={units.map(unit => ({ style={{ minWidth: "120px" }}
options={units.map((unit) => ({
label: unit.attributes.name, label: unit.attributes.name,
value: unit.attributes.name value: unit.attributes.name,
}))} }))}
onDropdownVisibleChange={(open) => { onDropdownVisibleChange={(open) => {
if (open) fetchUnits(); if (open) fetchUnits();
@@ -423,48 +487,32 @@ const SectionList = ({
dropdownRender={(menu) => ( dropdownRender={(menu) => (
<> <>
{menu} {menu}
<Divider style={{ margin: '8px 0' }} /> <Divider style={{ margin: "12px 0" }} />
<Select.Option value="ADD_NEW"> <div style={{ padding: "4px" }}>
<Button <Input.Search
type="text" placeholder="输入新单位名称"
icon={<PlusOutlined />} enterButton={<PlusOutlined />}
block onSearch={async (value) => {
onClick={(e) => { if (!value.trim()) return;
e.stopPropagation(); if (
Modal.confirm({ await handleAddUnit(value.trim())
title: '新增单位', ) {
content: ( const currentItems =
<Input form.getFieldValue([
placeholder="请输入单位名称" "sections",
onChange={(e) => { field.name,
Modal.confirm.update({ "items",
okButtonProps: { ]);
disabled: !e.target.value.trim() currentItems[itemField.name].unit =
} value.trim();
}); form.setFieldValue(
}} ["sections", field.name, "items"],
ref={(input) => { currentItems
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();
}
}
});
}} }}
> />
新增单位 </div>
</Button>
</Select.Option>
</> </>
)} )}
/> />
@@ -474,18 +522,34 @@ const SectionList = ({
name={[itemField.name, "quantity"]} name={[itemField.name, "quantity"]}
className="!mb-0" className="!mb-0"
> >
<InputNumber placeholder="数量" min={0} className="w-full" /> <InputNumber
placeholder="数量"
min={0}
className="w-full"
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...itemField} {...itemField}
name={[itemField.name, "price"]} name={[itemField.name, "price"]}
className="!mb-0" className="!mb-0"
> >
<InputNumber placeholder="单价" min={0} className="w-full" /> <InputNumber
placeholder="单价"
min={0}
className="w-full"
/>
</Form.Item> </Form.Item>
<ItemSubtotal <ItemSubtotal
quantity={formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.quantity} quantity={
price={formValues?.sections?.[sectionIndex]?.items?.[itemIndex]?.price} formValues?.sections?.[sectionIndex]?.items?.[
itemIndex
]?.quantity
}
price={
formValues?.sections?.[sectionIndex]?.items?.[
itemIndex
]?.price
}
currentCurrency={currentCurrency} currentCurrency={currentCurrency}
/> />
{!isView && itemFields.length > 1 && ( {!isView && itemFields.length > 1 && (
@@ -511,7 +575,7 @@ const SectionList = ({
</Button> </Button>
)} )}
<SectionTotal <SectionTotal
items={formValues?.sections?.[sectionIndex]?.items} items={formValues?.sections?.[sectionIndex]?.items}
currentCurrency={currentCurrency} currentCurrency={currentCurrency}
/> />
@@ -555,4 +619,5 @@ const SectionList = ({
); );
}; };
export default SectionList;
export default SectionList;

View File

@@ -18,16 +18,12 @@ import {
ArrowLeftOutlined, ArrowLeftOutlined,
SaveOutlined, SaveOutlined,
DeleteOutlined, DeleteOutlined,
CloseOutlined,
EditOutlined,
CheckOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { supabase } from "@/config/supabase"; import { supabase } from "@/config/supabase";
import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import SectionList from '@/components/SectionList'
const { TextArea } = Input; const { Title } = Typography;
const { Title, Text } = Typography;
// 添加货币符号映射 // 添加货币符号映射
const CURRENCY_SYMBOLS = { const CURRENCY_SYMBOLS = {
@@ -47,7 +43,7 @@ const QuotationForm = () => {
const [dataSource, setDataSource] = useState([{ id: Date.now() }]); const [dataSource, setDataSource] = useState([{ id: Date.now() }]);
const [totalAmount, setTotalAmount] = useState(0); const [totalAmount, setTotalAmount] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [currentCurrency, setCurrentCurrency] = useState("CNY"); const [currentCurrency, setCurrentCurrency] = useState("TWD");
const [customers, setCustomers] = useState([]); const [customers, setCustomers] = useState([]);
const [selectedCustomers, setSelectedCustomers] = useState([]); const [selectedCustomers, setSelectedCustomers] = useState([]);
const [formValues, setFormValues] = useState({}); const [formValues, setFormValues] = useState({});
@@ -737,304 +733,28 @@ const QuotationForm = () => {
</div> </div>
</Card> </Card>
{/* 报价单细卡片 */} <Card
<Form.List name="sections"> className="shadow-sm rounded-lg"
{(fields, { add, remove }) => ( type="inner"
<> title={
<div className="space-y-4"> <span className="flex items-center space-x-2 text-gray-700">
{fields.map((field, sectionIndex) => ( <span className="w-1 h-4 bg-blue-500 rounded-full" />
<Card <span>服务明细</span>
key={`section-${field.key}`} </span>
className="shadow-sm rounded-lg" }
type="inner" bordered={false}
title={ >
<div className="flex items-center justify-between"> <SectionList
<div className="flex items-center gap-2"> type="quotation"
{editingSectionIndex === sectionIndex ? ( form={form}
<div className="flex items-center gap-2"> isView={isView}
<Input formValues={formValues}
value={editingSectionName} currentCurrency={currentCurrency}
onChange={(e) => />
setEditingSectionName(e.target.value) </Card>
}
onPressEnter={handleSectionNameSave}
autoFocus
className="w-48"
/>
<Button
type="link"
icon={<CheckOutlined />}
onClick={handleSectionNameSave}
className="text-green-500 hover:text-green-600"
/>
<Button
type="link"
icon={<CloseOutlined />}
onClick={handleSectionNameCancel}
className="text-red-500 hover:text-red-600"
/>
</div>
) : (
<div className="flex items-center gap-2">
<span className="w-1 h-4 bg-purple-500 rounded-full" />
<Text
strong
className="text-lg dark:text-gray-200"
>
{form.getFieldValue([
"sections",
sectionIndex,
"sectionName",
]) || `服务类<EFBFBD><EFBFBD> ${sectionIndex + 1}`}
</Text>
{(!id || isEdit) && (
<Button
type="link"
icon={<EditOutlined />}
onClick={() =>
handleSectionNameEdit(
sectionIndex,
form.getFieldValue([
"sections",
sectionIndex,
"sectionName",
]) || `服务类型 ${sectionIndex + 1}`
)
}
className="text-gray-400 hover:text-blue-500"
/>
)}
</div>
)}
</div>
</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={`${sectionIndex}-${itemIndex}`}
className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-4 items-start"
>
<Form.Item
{...itemField}
name={[itemField.name, "productName"]}
className="!mb-0"
>
<Input placeholder="服务项目名称" />
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "note"]}
className="!mb-0"
>
<Input placeholder="请输入描述/备注" />
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "unit"]}
className="!mb-0"
>
<Input placeholder="单位" />
</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 && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => removeItem(itemField.name)}
className="flex items-center justify-center"
/>
)}
</div>
))}
{!isView && (
<Button
type="dashed"
onClick={() =>
handleAddItem(addItem, sectionIndex)
}
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>
{/* Add section button */}
{!isView && (
<div className="mt-6 flex justify-center">
<Button
type="dashed"
onClick={handleAddSection}
icon={<PlusOutlined />}
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500"
>
新建小节
</Button>
</div>
)}
{/* 总金额统计 */}
<div className="mt-6 space-y-4 pt-4 border-t">
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600 font-medium">税前总计:</span>
<span className="text-2xl font-semibold text-blue-600">
{formatCurrency(calculateTotalAmount(formValues?.sections))}
</span>
</div>
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600">税率:</span>
<div style={{ width: '150px' }}>
<Input
value={taxRate}
suffix="%"
onChange={(e) => {
const value = e.target.value.replace(/[^\d]/g, '');
setTaxRate(Number(value) || 0);
}}
/>
</div>
</div>
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600 font-medium">税后总计:</span>
<span className="text-xl font-semibold text-blue-600">
{formatCurrency(afterTaxAmount)}
</span>
</div>
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600">折扣价:</span>
<div style={{ width: '150px' }}>
<Input
value={discount}
prefix={CURRENCY_SYMBOLS[currentCurrency]}
onChange={(e) => {
const value = e.target.value.replace(/[^\d]/g, '');
setDiscount(Number(value) || 0);
}}
/>
</div>
</div>
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600 font-medium">最终金额:</span>
<span className="text-2xl font-semibold text-blue-600">
{formatCurrency(discount || afterTaxAmount)}
</span>
</div>
</div>
<Card
className="shadow-sm rounded-lg"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-purple-500 rounded-full" />
<span>补充说明</span>
</span>
}
bordered={false}
>
<Form.Item name="description" className="mb-0">
<TextArea
rows={4}
placeholder="请输入补充说明信息"
className="rounded-md hover:border-purple-400 focus:border-purple-500"
/>
</Form.Item>
</Card>
</>
)}
</Form.List>
</Form> </Form>
</Card> </Card>
<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>
</div> </div>
); );
}; };

View File

@@ -354,7 +354,8 @@ const QuotationPage = () => {
<AppstoreOutlined /> 使用选中模板 <AppstoreOutlined /> 使用选中模板
</Button>, </Button>,
]} ]}
width={800} width={900}
className="template-modal dark:bg-gray-800"
> >
{loading ? ( {loading ? (
<div className="flex justify-center items-center h-[400px]"> <div className="flex justify-center items-center h-[400px]">
@@ -363,63 +364,74 @@ const QuotationPage = () => {
) : templates.length === 0 ? ( ) : templates.length === 0 ? (
<Empty description="暂无可用模板" /> <Empty description="暂无可用模板" />
) : ( ) : (
<div className="max-h-[600px] overflow-y-auto px-1"> <div className="max-h-[600px] overflow-y-auto px-2">
{getTemplatesByCategory().map((group, groupIndex) => ( {getTemplatesByCategory().map((group, groupIndex) => (
<div key={groupIndex} className="mb-6 last:mb-2"> <div key={groupIndex} className="mb-8 last:mb-2">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-3 mb-4">
<div className="h-6 w-1 bg-blue-500 rounded-full"></div> <h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
<h3 className="text-base font-medium text-gray-700">
{group.name} {group.name}
<span className="ml-2 text-sm text-gray-400 font-normal"> <span className="ml-2 text-sm text-gray-500 dark:text-gray-400">
({group.templates.length}) ({group.templates.length})
</span> </span>
</h3> </h3>
</div> </div>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-4">
{group.templates.map(template => ( {group.templates.map(template => (
<div <div
key={template.id} key={template.id}
className={`
p-3 border rounded-lg cursor-pointer transition-all
${selectedTemplateId === template.id
? 'border-blue-500 bg-blue-50/50'
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50/50'
}
`}
onClick={() => handleTemplateSelect(template.id)} onClick={() => handleTemplateSelect(template.id)}
className={`
relative p-4 rounded-xl cursor-pointer transition-all duration-200
${
selectedTemplateId === template.id
? 'ring-2 ring-blue-500 bg-blue-50/40 dark:bg-blue-900/40'
: 'hover:bg-gray-50 dark:hover:bg-gray-700/50 border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md'
}
dark:bg-gray-800
`}
> >
<div className="flex justify-between items-start gap-2 mb-2"> <div className="flex justify-between items-start gap-3 mb-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h4 className="font-medium text-gray-800 truncate"> <h4 className="text-base font-medium text-gray-900 dark:text-gray-100 truncate">
{template.attributes.templateName} {template.attributes.templateName}
</h4> </h4>
<p className="text-xs text-gray-500 mt-1 line-clamp-1"> <p className="text-sm text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
{template.attributes.description || '暂无描述'} {template.attributes.description || '暂无描述'}
</p> </p>
</div> </div>
<span className="text-red-500 font-medium whitespace-nowrap text-sm"> <div className="text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap">
¥{template.attributes.totalAmount?.toLocaleString()} ¥{template.attributes.totalAmount?.toLocaleString()}
</span> </div>
</div> </div>
<div className="space-y-1"> <div className="space-y-2">
{template.attributes.sections.map((section, index) => ( {template.attributes.sections.map((section, index) => (
<div <div
key={index} key={index}
className="bg-white/80 px-2 py-1 rounded text-xs border border-gray-100" className="bg-white dark:bg-gray-700 rounded-lg p-2.5 text-sm border border-gray-100 dark:border-gray-600"
> >
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="font-medium text-blue-600 truncate flex-1"> <span className="font-medium text-gray-700 dark:text-gray-200 truncate flex-1">
{section.sectionName} {section.sectionName}
</span> </span>
<span className="text-gray-400 ml-1"> <span className="text-gray-500 dark:text-gray-400 ml-2 text-xs">
{section.items.length} {section.items.length}
</span> </span>
</div> </div>
</div> </div>
))} ))}
</div> </div>
{selectedTemplateId === template.id && (
<div className="absolute top-3 right-3">
<div className="w-5 h-5 bg-blue-500 dark:bg-blue-600 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
</svg>
</div>
</div>
)}
</div> </div>
))} ))}
</div> </div>

View File

@@ -31,7 +31,6 @@ const QuotationTemplate = ({ id, isView, onCancel,isEdit }) => {
'attributes->>template_type': { eq: TYPE } 'attributes->>template_type': { eq: TYPE }
} }
}); });
console.log(data,'data');
if (data?.[0]) { if (data?.[0]) {
const formData = { const formData = {

View File

@@ -38,7 +38,7 @@ const ServiceForm = () => {
} }
return ( return (
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2"> <div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2" >
<Card <Card
className="shadow-lg rounded-lg border-0" className="shadow-lg rounded-lg border-0"
title={ title={

View File

@@ -42,23 +42,26 @@ const ResourceManagement = () => {
onChange={setActiveType} onChange={setActiveType}
type="card" type="card"
className="bg-white rounded-lg shadow-sm" className="bg-white rounded-lg shadow-sm"
items={TEMPLATE_TYPES.map(type => ({ items={TEMPLATE_TYPES.map(type => {
key: type.key, return ({
label: ( key: type.key,
<span className="flex items-center gap-2"> label: (
{type.icon} <span className="flex items-center gap-2">
<span>{type.label}</span> {type.icon}
</span> <span>{type.label}</span>
), </span>
children: ( ),
<div className="p-6"> children: (
<Card> <div className="p-6">
<Classify typeList={filterOption} activeType={activeType} setActiveType={setActiveType} /> <Card>
<Unit typeList={filterOption} activeType={activeType} /> <Classify typeList={filterOption} activeType={activeType} setActiveType={setActiveType} />
</Card> <Unit typeList={filterOption} activeType={activeType} />
</div> <Sections typeList={filterOption} activeType={activeType} />
) </Card>
}))} </div>
)
});
})}
/> />

View File

@@ -1,127 +1,127 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Table, Button, Drawer, Modal, Form, Input, InputNumber, Space, message, Popconfirm, Select } from 'antd'; import {
import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'; Table,
import { supabase } from '@/config/supabase'; Button,
Form,
Input,
Space,
message,
Popconfirm,
Select,
Segmented,
InputNumber,
Card,
Typography
} from 'antd';
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
import { supabaseService } from '@/hooks/supabaseService';
import { v4 as uuidv4 } from 'uuid';
const SectionManagement = () => { const { Text } = Typography;
const SectionsManagement = ({ activeType = 'quotation', typeList }) => {
const [data, setData] = useState([]); const [data, setData] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [drawerVisible, setDrawerVisible] = useState(false); const [editingKey, setEditingKey] = useState('');
const [modalVisible, setModalVisible] = useState(false);
const [editingRecord, setEditingRecord] = useState(null);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [units, setUnits] = useState([]); const [filterType, setFilterType] = useState('all');
// 获取子模块数据 const fetchSections = async (type = activeType, filterTypeValue = filterType) => {
const fetchSections = async () => {
setLoading(true); setLoading(true);
try { try {
const { data: sections, error } = await supabase let filterCondition;
.from('resources')
.select('*') switch (filterTypeValue) {
.eq('type', 'sections'); case 'current':
filterCondition = { eq: type };
break;
default:
filterCondition = { eq: type };
}
if (error) throw error; const { data: sections } = await supabaseService.select('resources', {
filter: {
'type': { eq: 'sections' },
'attributes->>template_type': filterCondition
},
order: {
column: 'created_at',
ascending: false
}
});
setData(sections || []); setData(sections || []);
} catch (error) { } catch (error) {
message.error('获取模块数据失败'); message.error('获取模块数据失败');
console.error(error); console.error(error);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
// 获取单位数据
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;
setUnits(unitsData || []);
} catch (error) {
message.error('获取单位数据失败');
console.error(error);
}
};
useEffect(() => { useEffect(() => {
if (drawerVisible) { fetchSections(activeType, filterType);
fetchSections(); }, [activeType]);
}
fetchUnits();
}, [drawerVisible]);
// 打开新增/编辑模态框 const handleAdd = () => {
const showModal = async (record = null) => { const newData = {
setModalVisible(true); id: Date.now().toString(),
setEditingRecord(record); attributes: {
if (record) {
try {
const { data: section, error } = await supabase
.from('resources')
.select('*')
.eq('id', record.id)
.single();
if (error) throw error;
form.setFieldsValue({
name: section.attributes.name,
items: section.attributes.items
});
} catch (error) {
message.error('获取子模块详情失败');
console.error(error);
}
} else {
form.setFieldsValue({
name: '', name: '',
items: [{ name: '', description: '', price: 0, quantity: 1, unit: '' }] template_type: activeType,
}); items: [{
} key: uuidv4(),
name: '',
description: '',
quantity: 1,
price: 0,
unit: ''
}]
},
isNew: true
};
setData([newData, ...data]);
setEditingKey(newData.id);
form.setFieldsValue(newData.attributes);
}; };
// 保存子模块数据 const handleSave = async (record) => {
const handleSave = async (values) => {
try { try {
if (editingRecord) { const values = await form.validateFields();
// 更新 const items = form.getFieldValue(['items']) || [];
const { error } = await supabase
.from('resources') // 验证items数组
.update({ if (!items.length || !items.some(item => item.name)) {
message.error('请至少添加一个有效的服务项目');
return;
}
if (record.isNew) {
await supabaseService.insert('resources', {
type: 'sections',
attributes: {
name: values.name,
template_type: activeType,
items: items.filter(item => item.name), // 只保存有名称的项目
},
schema_version: 1
});
} else {
await supabaseService.update('resources',
{ id: record.id },
{
attributes: { attributes: {
name: values.name, name: values.name,
items: values.items template_type: activeType,
items: items.filter(item => item.name),
}, },
updated_at: new Date().toISOString() updated_at: new Date().toISOString()
}) }
.eq('id', editingRecord.id); );
if (error) throw error;
} else {
// 新增
const { error } = await supabase
.from('resources')
.insert([{
type: 'sections',
attributes: {
name: values.name,
items: values.items
},
schema_version: 1
}]);
if (error) throw error;
} }
message.success('保存成功'); message.success('保存成功');
setModalVisible(false); setEditingKey('');
form.resetFields();
fetchSections(); fetchSections();
} catch (error) { } catch (error) {
message.error('保存失败'); message.error('保存失败');
@@ -129,15 +129,9 @@ const SectionManagement = () => {
} }
}; };
// 删除子模块 const handleDelete = async (record) => {
const handleDelete = async (id) => {
try { try {
const { error } = await supabase await supabaseService.delete('resources', { id: record.id });
.from('resources')
.delete()
.eq('id', id);
if (error) throw error;
message.success('删除成功'); message.success('删除成功');
fetchSections(); fetchSections();
} catch (error) { } catch (error) {
@@ -146,290 +140,226 @@ const SectionManagement = () => {
} }
}; };
const drawerColumns = [ const columns = [
{ {
title: '项目名称', title: '模块名称',
dataIndex: 'name', dataIndex: ['attributes', 'name'],
}, width: 200,
{ render: (text, record) => {
title: '描述', const isEditing = record.id === editingKey;
dataIndex: 'description', return isEditing ? (
},
{
title: '单价',
dataIndex: 'price',
render: (price) => `¥${price}`
},
{
title: '数量',
dataIndex: 'quantity',
},
{
title: '单位',
dataIndex: 'unit',
}
];
// 添加模态框内表格列定义
const modalColumns = [
{
title: '项目名称',
dataIndex: 'name',
render: (_, __, index) => (
<Form.Item
name={[index, 'name']}
style={{ margin: 0 }}
>
<Input placeholder="项目名称" />
</Form.Item>
)
},
{
title: '描述',
dataIndex: 'description',
render: (_, __, index) => (
<Form.Item
name={[index, 'description']}
style={{ margin: 0 }}
>
<Input placeholder="描述" />
</Form.Item>
)
},
{
title: '单价',
dataIndex: 'price',
render: (_, __, index) => (
<Form.Item
name={[index, 'price']}
rules={[{ required: true, message: '请输入单价!' }]}
style={{ margin: 0 }}
>
<InputNumber
min={0}
placeholder="单价"
className="w-full"
/>
</Form.Item>
)
},
{
title: '数量',
dataIndex: 'quantity',
render: (_, __, index) => (
<Form.Item
name={[index, 'quantity']}
rules={[{ required: true, message: '请输入数量!' }]}
style={{ margin: 0 }}
>
<InputNumber
min={1}
placeholder="数量"
className="w-full"
/>
</Form.Item>
)
},
{
title: '单位',
dataIndex: 'unit',
render: (_, __, index) => (
<Form.Item
name={[index, 'unit']}
rules={[{ required: true, message: '请选择或输入单位!' }]}
style={{ margin: 0 }}
>
<Select
placeholder="请选择或输入单位"
className="w-full"
showSearch
allowClear
options={units.map(unit => ({
label: unit.attributes.name,
value: unit.attributes.name
}))}
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
/>
</Form.Item>
)
},
{
title: '操作',
render: (_, __, index, { remove }) => (
<Button
type="link"
danger
icon={<DeleteOutlined />}
onClick={() => remove(index)}
/>
)
}
];
return (
<div>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setDrawerVisible(true)}
className="flex items-center"
>
子模块管理
</Button>
<Drawer
title={
<span className="text-lg font-medium text-gray-800 dark:text-gray-200">
子模块管理
</span>
}
placement="right"
width={1000}
onClose={() => setDrawerVisible(false)}
open={drawerVisible}
className="dark:bg-gray-800"
>
<div className="flex flex-col h-full">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => showModal()}
className="mb-4 w-32 flex items-center justify-center"
>
新增子模块
</Button>
<div className="space-y-6">
{data.map((section) => (
<div
key={section.id}
className="bg-white rounded-lg shadow-sm dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
>
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 className="text-lg font-medium text-gray-800 dark:text-gray-200">
{section.attributes.name}
</h3>
<Space>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => showModal(section)}
className="text-blue-600 hover:text-blue-500"
>
编辑
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => handleDelete(section.id)}
okButtonProps={{
className: "bg-red-500 hover:bg-red-600 border-red-500"
}}
>
<Button
type="link"
danger
icon={<DeleteOutlined />}
className="text-red-600 hover:text-red-500"
/>
</Popconfirm>
</Space>
</div>
<div className="p-4">
<Table
scroll={{ x: true }}
dataSource={section.attributes.items}
columns={drawerColumns}
pagination={false}
rowKey={(record, index) => `${section.id}-${index}`}
className="border dark:border-gray-700 rounded-lg"
rowClassName="hover:bg-gray-50 dark:hover:bg-gray-700/50"
size="small"
/>
</div>
</div>
))}
</div>
</div>
</Drawer>
<Modal
title={`${editingRecord ? '编辑' : '新增'}子模块`}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
setEditingRecord(null);
form.resetFields();
}}
footer={null}
width={1200}
destroyOnClose={true}
>
<Form
form={form}
onFinish={handleSave}
layout="vertical"
className="mt-4"
>
<Form.Item <Form.Item
name="name" name="name"
label="子模块名称" style={{ margin: 0 }}
rules={[{ required: true, message: '请输入模块名称!' }]} rules={[{ required: true, message: '请输入模块名称!' }]}
> >
<Input placeholder="请输入子模块名称" /> <Input
placeholder="请输入模块名称"
className="rounded-md"
/>
</Form.Item> </Form.Item>
) : (
<Form.List name="items"> <span className="text-gray-700 font-medium">{text}</span>
{(fields, { add, remove }) => ( );
<div className="bg-white rounded-lg border dark:bg-gray-800 dark:border-gray-700"> },
<Table },
dataSource={fields} {
columns={modalColumns.map(col => ({ title: '服务项目',
...col, dataIndex: ['attributes', 'items'],
render: (...args) => col.render(...args, { remove }) render: (items, record) => {
}))} const isEditing = record.id === editingKey;
pagination={false}
rowKey="key" if (isEditing) {
className="mb-4" return (
/> <Form.List name="items">
<div className="p-4 border-t dark:border-gray-700"> {(fields, { add, remove }) => (
<div className="space-y-2">
{fields.map((field, index) => (
<Card key={field.key} size="small" className="bg-gray-50">
<div className="grid grid-cols-6 gap-2">
<Form.Item
{...field}
name={[field.name, 'name']}
className="col-span-2 mb-0"
>
<Input placeholder="项目名称" />
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'unit']}
className="mb-0"
>
<Input placeholder="单位" />
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'quantity']}
className="mb-0"
>
<InputNumber
placeholder="数量"
min={0}
className="w-full"
/>
</Form.Item>
<Form.Item
{...field}
name={[field.name, 'price']}
className="mb-0"
>
<InputNumber
placeholder="单价"
min={0}
className="w-full"
/>
</Form.Item>
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => remove(field.name)}
className="flex items-center justify-center"
/>
</div>
</Card>
))}
<Button <Button
type="dashed" type="dashed"
onClick={() => add({ onClick={() => add({
key: uuidv4(),
name: '', name: '',
description: '', unit: '',
price: 0,
quantity: 1, quantity: 1,
unit: '' price: 0
})} })}
icon={<PlusOutlined />} className="w-full"
className="w-full hover:border-blue-400 hover:text-blue-500
dark:border-gray-600 dark:text-gray-400 dark:hover:text-blue-400"
> >
添加项目 <PlusOutlined /> 添加服务项目
</Button> </Button>
</div> </div>
</div> )}
)} </Form.List>
</Form.List> );
}
<div className="flex justify-end gap-4 mt-6"> return (
<Button onClick={() => { <div className="space-y-1">
setModalVisible(false); {(items || []).map((item, index) => (
setEditingRecord(null); <div key={index} className="flex justify-between text-sm">
form.resetFields(); <span className="text-gray-600">{item.name}</span>
}}> <span className="text-gray-500">
取消 {item.quantity} {item.unit} × ¥{item.price}
</Button> </span>
<Button type="primary" htmlType="submit"> </div>
))}
</div>
);
},
},
{
title: '操作',
width: 160,
fixed: 'right',
render: (_, record) => {
const isEditing = record.id === editingKey;
return isEditing ? (
<Space>
<Button
type="link"
className="text-green-600 hover:text-green-500 font-medium"
onClick={() => handleSave(record)}
>
保存 保存
</Button> </Button>
<Button
type="link"
className="text-gray-600 hover:text-gray-500 font-medium"
onClick={() => {
setEditingKey('');
if (record.isNew) {
setData(data.filter(item => item.id !== record.id));
}
}}
>
取消
</Button>
</Space>
) : (
<Space>
<Button
type="link"
className="text-blue-600 hover:text-blue-500 font-medium"
disabled={editingKey !== ''}
onClick={() => {
setEditingKey(record.id);
form.setFieldsValue({
name: record.attributes.name,
items: record.attributes.items
});
}}
>
编辑
</Button>
<Popconfirm
title="确认删除"
description="确定要删除这个模块吗?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
okButtonProps={{
className: "bg-red-500 hover:bg-red-600 border-red-500"
}}
>
<Button
type="link"
className="text-red-600 hover:text-red-500 font-medium"
disabled={editingKey !== ''}
>
删除
</Button>
</Popconfirm>
</Space>
);
},
},
];
return (
<div className="p-6 bg-gray-50">
<div className="bg-white rounded-lg shadow-sm mb-6 p-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="flex items-center gap-2">
<Button
type="primary"
onClick={handleAdd}
icon={<PlusOutlined />}
className="bg-blue-600 hover:bg-blue-700 border-0 shadow-sm h-10"
>
新增模块
</Button>
</div> </div>
</div>
</div>
<div className="bg-white rounded-lg shadow-sm">
<Form form={form}>
<Table
scroll={{ x: 1200 }}
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
className: "px-4"
}}
className="rounded-lg"
/>
</Form> </Form>
</Modal> </div>
</div> </div>
); );
}; };
export default SectionManagement; export default SectionsManagement;

View File

@@ -79,7 +79,6 @@ export const TeamTable = ({ tableLoading,pagination,dataSource, onTableChange,on
}, },
{ {
title: '归属', title: '归属',
dataIndex: 'type',
dataIndex: ["attributes", "type"], dataIndex: ["attributes", "type"],
key: "type", key: "type",
}, },