quata模块完成

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

View File

@@ -1,16 +1,35 @@
import { Card, Table, Button, Space, Input, message, Popconfirm, Form, Tag, Typography } from "antd";
import { DownOutlined, UpOutlined,EyeOutlined,EditOutlined } from "@ant-design/icons";
import {
Card,
Table,
Button,
Space,
Input,
message,
Popconfirm,
Form,
Tag,
Typography,
} from "antd";
import {
DownOutlined,
UpOutlined,
EyeOutlined,
EditOutlined,
} from "@ant-design/icons";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { supabase } from "@/config/supabase";
import UnitManagement from "@/pages/company/service/unit";
import Sections from "@/pages/company/service/sections";
import CategoryDrawer from "@/pages/company/service/classify";
const { Paragraph } = Typography;
const ServicePage = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [categories, setCategories] = useState([]);
const navigate = useNavigate();
const [editingKey, setEditingKey] = useState('');
const [editingKey, setEditingKey] = useState("");
// 获取服务模板列表
const fetchServices = async () => {
@@ -32,19 +51,35 @@ const ServicePage = () => {
}
};
// 添加获取分类数据的方法
const fetchCategories = async () => {
try {
const { data: categoriesData, error } = await supabase
.from('resources')
.select('*')
.eq('type', 'categories')
.order('created_at', { ascending: false });
if (error) throw error;
setCategories(categoriesData || []);
} catch (error) {
console.error('获取分类数据失败:', error);
message.error('获取分类数据失败');
}
};
// 修改保存逻辑
const handleSaveItem = async (record) => {
try {
setLoading(true);
const { serviceId, sectionIndex, itemIndex } = record;
// 找到当前服务模板
const currentService = data.find(item => item.id === serviceId);
const currentService = data.find((item) => item.id === serviceId);
if (!currentService) {
throw new Error('Service not found');
throw new Error("Service not found");
}
// 创建新的 attributes 对象,避免直接修改状态
const newAttributes = {
...currentService.attributes,
templateName: currentService.attributes.templateName,
@@ -60,47 +95,57 @@ const ServicePage = () => {
name: record.name,
price: Number(record.price) || 0,
quantity: Number(record.quantity) || 0,
description: record.description || '',
unit: record.unit || ''
description: record.description || "",
unit: record.unit || "",
};
}
return item;
})
}),
};
}
return section;
})
}),
};
// 重新计算总金额
newAttributes.totalAmount = newAttributes.sections.reduce((total, section) => {
return total + section.items.reduce((sectionTotal, item) =>
sectionTotal + ((Number(item.price) || 0) * (Number(item.quantity) || 0)), 0);
}, 0);
newAttributes.totalAmount = newAttributes.sections.reduce(
(total, section) => {
return (
total +
section.items.reduce(
(sectionTotal, item) =>
sectionTotal +
(Number(item.price) || 0) * (Number(item.quantity) || 0),
0
)
);
},
0
);
// 调用 supabase 更新数据
const { error } = await supabase
.from('resources')
.update({
.from("resources")
.update({
type: "serviceTemplate",
attributes: newAttributes
attributes: newAttributes,
})
.eq('id', serviceId);
.eq("id", serviceId);
if (error) throw error;
// 更新本地状态
setData(prevData => prevData.map(item =>
item.id === serviceId
? { ...item, attributes: newAttributes }
: item
));
setData((prevData) =>
prevData.map((item) =>
item.id === serviceId ? { ...item, attributes: newAttributes } : item
)
);
setEditingKey('');
message.success('保存成功');
setEditingKey("");
message.success("保存成功");
} catch (error) {
console.error('保存失败:', error);
message.error('保存失败');
console.error("保存失败:", error);
message.error("保存失败");
} finally {
setLoading(false);
}
@@ -111,11 +156,11 @@ const ServicePage = () => {
try {
setLoading(true);
const { serviceId, sectionIndex, itemIndex } = record;
// 找到目标服务
const targetService = data.find(item => item.id === serviceId);
if (!targetService) throw new Error('Service not found');
const targetService = data.find((item) => item.id === serviceId);
if (!targetService) throw new Error("Service not found");
// 创建新的 attributes 对象,避免直接修改状态
const newAttributes = {
...targetService.attributes,
@@ -123,40 +168,51 @@ const ServicePage = () => {
if (secIdx === sectionIndex) {
return {
...section,
items: section.items.filter((_, idx) => idx !== itemIndex)
items: section.items.filter((_, idx) => idx !== itemIndex),
};
}
return section;
})
}),
};
// 重新计算总金额
newAttributes.totalAmount = newAttributes.sections.reduce((total, section) => {
return total + section.items.reduce((sectionTotal, item) =>
sectionTotal + ((Number(item.price) || 0) * (Number(item.quantity) || 0)), 0);
}, 0);
newAttributes.totalAmount = newAttributes.sections.reduce(
(total, section) => {
return (
total +
section.items.reduce(
(sectionTotal, item) =>
sectionTotal +
(Number(item.price) || 0) * (Number(item.quantity) || 0),
0
)
);
},
0
);
// 调用 supabase 更新数据
const { error } = await supabase
.from('resources')
.update({
attributes: newAttributes
.from("resources")
.update({
attributes: newAttributes,
})
.eq('id', serviceId);
.eq("id", serviceId);
if (error) throw error;
// 更新本地状态
setData(prevData => prevData.map(item =>
item.id === serviceId
? { ...item, attributes: newAttributes }
: item
));
setData((prevData) =>
prevData.map((item) =>
item.id === serviceId ? { ...item, attributes: newAttributes } : item
)
);
message.success('删除成功');
message.success("删除成功");
} catch (error) {
console.error('删除失败:', error);
message.error('删<><E588A0><EFBFBD>失败');
console.error("删除失败:", error);
message.error("删除失败");
} finally {
setLoading(false);
}
@@ -166,21 +222,21 @@ const ServicePage = () => {
const handleDeleteService = async (serviceId) => {
try {
setLoading(true);
// 调用 supabase 删除数据
const { error } = await supabase
.from('resources')
.from("resources")
.delete()
.eq('id', serviceId);
.eq("id", serviceId);
if (error) throw error;
// 更新本地状态
setData(prevData => prevData.filter(item => item.id !== serviceId));
message.success('删除成功');
setData((prevData) => prevData.filter((item) => item.id !== serviceId));
message.success("删除成功");
} catch (error) {
console.error('删除失败:', error);
message.error('删除失败');
console.error("删除失败:", error);
message.error("删除失败");
} finally {
setLoading(false);
}
@@ -198,38 +254,39 @@ const ServicePage = () => {
title: "分类",
dataIndex: ["attributes", "category"],
key: "category",
render: (category) => {
if (!category || !Array.isArray(category)) return null;
render: (categories) => {
if (!categories || !Array.isArray(categories)) return null;
return (
<Paragraph
ellipsis={{
rows: 1,
tooltip: (
<Space size={[0, 8]} wrap>
{category.map((cat, index) => (
{categories.map((category) => (
<Tag
size="small"
key={`${cat}-${index}`}
key={category.id}
color="blue"
className="px-2 py-1 rounded-md"
>
{cat}
{category.name}
</Tag>
))}
</Space>
)
),
}}
className="mb-0"
>
<Space size={[0, 8]} nowrap>
{category.map((cat, index) => (
{categories.map((category) => (
<Tag
size="small"
key={`${cat}-${index}`}
key={category.id}
color="blue"
className="px-2 py-1 rounded-md"
>
{cat}
{category.name}
</Tag>
))}
</Space>
@@ -241,18 +298,18 @@ const ServicePage = () => {
title: "描述",
dataIndex: ["attributes", "description"],
key: "description",
width: 300,
className: "min-w-[100px] ",
render: (text) => (
<Paragraph
ellipsis={{
rows: 2,
tooltip: text
tooltip: text,
}}
className="mb-0"
>
{text}
</Paragraph>
)
),
},
{
title: "总金额",
@@ -260,7 +317,7 @@ const ServicePage = () => {
key: "totalAmount",
className: "min-w-[150px] text-right",
render: (amount) => (
<span style={{ color: '#f50', fontWeight: 'bold' }}>
<span style={{ color: "#f50", fontWeight: "bold" }}>
¥
{amount?.toLocaleString("zh-CN", {
minimumFractionDigits: 2,
@@ -272,21 +329,25 @@ const ServicePage = () => {
{
title: "操作",
key: "action",
fixed: 'right',
fixed: "right",
width: 220,
render: (_, record) => (
<Space onClick={e => e.stopPropagation()}>
<Space onClick={(e) => e.stopPropagation()}>
<Button
type="link"
icon={<EyeOutlined />}
onClick={() => navigate(`/company/serviceTemplateInfo/${record.id}`)}
onClick={() =>
navigate(`/company/serviceTemplateInfo/${record.id}`)
}
>
查看
</Button>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => navigate(`/company/serviceTemplateInfo/${record.id}?edit=true`)}
onClick={() =>
navigate(`/company/serviceTemplateInfo/${record.id}?edit=true`)
}
>
编辑
</Button>
@@ -298,11 +359,7 @@ const ServicePage = () => {
okButtonProps={{ danger: true }}
onConfirm={() => handleDeleteService(record.id)}
>
<Button
type="link"
danger
className="text-red-600"
>
<Button type="link" danger className="text-red-600">
删除
</Button>
</Popconfirm>
@@ -326,19 +383,26 @@ const ServicePage = () => {
name={[record.sectionIndex, "items", record.itemIndex, "name"]}
className="!mb-0"
>
<Input
value={text}
onChange={e => {
<Input
value={text}
onChange={(e) => {
const newData = [...data];
const targetService = newData.find(item => item.id === record.serviceId);
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
const targetService = newData.find(
(item) => item.id === record.serviceId
);
const targetItem =
targetService.attributes.sections[record.sectionIndex].items[
record.itemIndex
];
targetItem.name = e.target.value;
setData(newData);
}}
/>
</Form.Item>
) : text;
}
) : (
text
);
},
},
{
title: "描述",
@@ -349,16 +413,25 @@ const ServicePage = () => {
const isEditing = record.key === editingKey;
return isEditing ? (
<Form.Item
name={[record.sectionIndex, "items", record.itemIndex, "description"]}
name={[
record.sectionIndex,
"items",
record.itemIndex,
"description",
]}
className="!mb-0"
>
<Input.TextArea
rows={1}
value={text}
onChange={e => {
<Input
value={text}
onChange={(e) => {
const newData = [...data];
const targetService = newData.find(item => item.id === record.serviceId);
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
const targetService = newData.find(
(item) => item.id === record.serviceId
);
const targetItem =
targetService.attributes.sections[record.sectionIndex].items[
record.itemIndex
];
targetItem.description = e.target.value;
setData(newData);
}}
@@ -368,14 +441,14 @@ const ServicePage = () => {
<Paragraph
ellipsis={{
rows: 2,
tooltip: text
tooltip: text,
}}
className="mb-0"
>
{text}
</Paragraph>
);
}
},
},
{
title: "单价",
@@ -385,26 +458,32 @@ const ServicePage = () => {
render: (price, record) => {
const isEditing = record.key === editingKey;
return isEditing ? (
<Input
<Input
type="number"
value={price}
onChange={e => {
value={price}
onChange={(e) => {
const newData = [...data];
const targetService = newData.find(item => item.id === record.serviceId);
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
const targetService = newData.find(
(item) => item.id === record.serviceId
);
const targetItem =
targetService.attributes.sections[record.sectionIndex].items[
record.itemIndex
];
targetItem.price = Number(e.target.value);
setData(newData);
}}
/>
) : (
<span className="text-gray-600">
¥{price?.toLocaleString("zh-CN", {
¥
{price?.toLocaleString("zh-CN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</span>
);
}
},
},
{
title: "数量",
@@ -414,19 +493,26 @@ const ServicePage = () => {
render: (quantity, record) => {
const isEditing = record.key === editingKey;
return isEditing ? (
<Input
<Input
type="number"
value={quantity}
onChange={e => {
value={quantity}
onChange={(e) => {
const newData = [...data];
const targetService = newData.find(item => item.id === record.serviceId);
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
const targetService = newData.find(
(item) => item.id === record.serviceId
);
const targetItem =
targetService.attributes.sections[record.sectionIndex].items[
record.itemIndex
];
targetItem.quantity = Number(e.target.value);
setData(newData);
}}
/>
) : <span className="text-gray-600">{quantity}</span>;
}
) : (
<span className="text-gray-600">{quantity}</span>
);
},
},
{
title: "小计",
@@ -446,37 +532,39 @@ const ServicePage = () => {
),
},
{
title: '操作',
key: 'action',
title: "操作",
key: "action",
width: "150px",
render: (_, record) => {
const isEditing = record.key === editingKey;
return isEditing ? (
<Space>
<Button
type="link"
<Button
type="link"
size="small"
className="text-green-600"
onClick={() => handleSaveItem(record, record.sectionIndex, record.itemIndex)}
onClick={() =>
handleSaveItem(record, record.sectionIndex, record.itemIndex)
}
>
保存
</Button>
<Button
type="link"
<Button
type="link"
size="small"
className="text-gray-600"
onClick={() => setEditingKey('')}
onClick={() => setEditingKey("")}
>
取消
</Button>
</Space>
) : (
<Space>
<Button
type="link"
<Button
type="link"
size="small"
className="text-blue-600"
disabled={editingKey !== ''}
disabled={editingKey !== ""}
onClick={() => setEditingKey(record.key)}
>
编辑
@@ -489,19 +577,19 @@ const ServicePage = () => {
okButtonProps={{ danger: true }}
onConfirm={() => handleDeleteItem(record)}
>
<Button
type="link"
<Button
type="link"
size="small"
className="text-red-600"
disabled={editingKey !== ''}
disabled={editingKey !== ""}
>
删除
</Button>
</Popconfirm>
</Space>
);
}
}
},
},
];
// 修改 expandedRowRender 函数,确保 key 的唯一性
@@ -520,8 +608,9 @@ const ServicePage = () => {
总计: ¥
{section.items
.reduce(
(total, item) =>
total + (Number(item.price) || 0) * (Number(item.quantity) || 0),
(total, item) =>
total +
(Number(item.price) || 0) * (Number(item.quantity) || 0),
0
)
.toLocaleString("zh-CN", {
@@ -532,11 +621,11 @@ const ServicePage = () => {
</div>
}
headStyle={{
background: 'rgba(59, 130, 246, 0.05)',
borderBottom: '1px solid rgba(59, 130, 246, 0.1)'
background: "rgba(59, 130, 246, 0.05)",
borderBottom: "1px solid rgba(59, 130, 246, 0.1)",
}}
bodyStyle={{
background: 'rgba(59, 130, 246, 0.02)'
background: "rgba(59, 130, 246, 0.02)",
}}
>
<Table
@@ -546,7 +635,7 @@ const ServicePage = () => {
key: `${record.id}-${sectionIndex}-${itemIndex}`,
serviceId: record.id,
sectionIndex,
itemIndex
itemIndex,
}))}
pagination={false}
className="rounded-lg overflow-hidden"
@@ -559,6 +648,7 @@ const ServicePage = () => {
useEffect(() => {
fetchServices();
fetchCategories();
}, []);
return (
@@ -567,12 +657,18 @@ const ServicePage = () => {
title={<span className="text-xl font-medium">服务模版管理</span>}
className="h-full w-full overflow-auto"
extra={
<Button
type="primary"
onClick={() => navigate("/company/serviceTemplateInfo")}
>
新建模版
</Button>
<Space>
<Sections />
<UnitManagement />
<CategoryDrawer />
<Button
type="primary"
onClick={() => navigate("/company/serviceTemplateInfo")}
>
新建模版
</Button>
</Space>
}
>
<Table
@@ -587,29 +683,34 @@ const ServicePage = () => {
expandIcon: ({ expanded, onExpand, record }) => (
<Button
type="link"
onClick={e => {
onClick={(e) => {
e.stopPropagation();
onExpand(record, e);
}}
icon={expanded ? <UpOutlined /> : <DownOutlined />}
className={`
transition-all duration-300
${expanded
? 'text-blue-600'
: 'text-gray-400 hover:text-blue-600'
${
expanded
? "text-blue-600"
: "text-gray-400 hover:text-blue-600"
}
`}
/>
),
rowExpandable: record => record.attributes.sections?.length > 0,
rowExpandable: (record) => record.attributes.sections?.length > 0,
}}
className="rounded-lg overflow-hidden"
rowClassName={(record, index) => `
cursor-pointer transition-colors
${index % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-750'}
${
index % 2 === 0
? "bg-white dark:bg-gray-800"
: "bg-gray-50 dark:bg-gray-750"
}
hover:bg-blue-50 dark:hover:bg-blue-900/20
`}
expandedRowClassName={() => 'bg-blue-50/50 dark:bg-blue-900/10'}
expandedRowClassName={() => "bg-blue-50/50 dark:bg-blue-900/10"}
/>
</Card>
</div>