Files
manage/src/pages/company/service/index.jsx
‘Liammcl’ b3ac241354 fix
2024-12-28 12:37:46 +08:00

603 lines
18 KiB
JavaScript

import { useState, useEffect } from "react";
import {
Card,
Table,
Button,
Space,
Badge,
message,
Popconfirm,
Tag,
Divider,
Input,
InputNumber,
} from "antd";
import {
PlusOutlined,
FileTextOutlined,
ProjectOutlined,
CheckSquareOutlined,
DeleteOutlined,
EditOutlined,
SaveOutlined,
CloseOutlined,
} from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import { supabaseService } from "@/hooks/supabaseService";
import TemplateTypeModal from "@/components/TemplateTypeModal";
const ServicePage = () => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [selectedType, setSelectedType] = useState(null);
const [editingKey, setEditingKey] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const navigate = useNavigate();
// 模板类型配置
const TEMPLATE_TYPES = {
quotation: {
label: "报价单模板",
icon: <FileTextOutlined />,
color: "blue",
path: "/company/serviceTemplateInfo",
},
project: {
label: "专案模板",
icon: <ProjectOutlined />,
color: "green",
path: "/company/serviceTemplateInfo",
},
task: {
label: "任务模板",
icon: <CheckSquareOutlined />,
color: "purple",
path: "/company/serviceTemplateInfo",
},
};
// 获取服务模板列表
const fetchServices = async () => {
try {
setLoading(true);
const { data: services } = await supabaseService.select("resources", {
filter: {
type: { eq: "serviceTemplate" },
...(selectedType
? { "attributes->>template_type": { eq: selectedType } }
: {}),
},
order: {
column: "created_at",
ascending: false,
},
});
setData(services || []);
} catch (error) {
console.error("获取服务模板失败:", error);
message.error("获取服务模板失败");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchServices();
}, [selectedType]);
// 删除服务模板
const handleDeleteService = async (serviceId) => {
try {
await supabaseService.delete("resources", { id: serviceId });
message.success("删除成功");
fetchServices();
} catch (error) {
console.error("删除失败:", error);
message.error("删除失败");
}
};
// 添加行内编辑相关方法
const handleSave = async (record, sectionKey, itemIndex) => {
try {
if (!editingItem) {
message.error("没有要保存的数据");
return;
}
const newData = [...data];
const targetIndex = newData.findIndex((item) => item.id === record.id);
if (targetIndex > -1) {
const item = newData[targetIndex];
const sections = [...item.attributes.sections];
const sectionIndex = sections.findIndex((s) => s.key === sectionKey);
if (sectionIndex > -1) {
// 使用 editingItem 中的值更新项目
sections[sectionIndex].items[itemIndex] = editingItem;
const updatedData = {
...item,
attributes: {
...item.attributes,
sections,
},
};
await supabaseService.update(
"resources",
{ id: record.id },
{ attributes: updatedData.attributes }
);
newData[targetIndex] = updatedData;
setData(newData);
setEditingKey("");
setEditingItem(null);
message.success("保存成功");
}
}
} catch (error) {
console.error("保存失败:", error);
message.error("保存失败");
}
};
const handleDeleteSection = async (record, sectionKey) => {
try {
const newData = [...data];
const targetIndex = newData.findIndex((item) => item.id === record.id);
if (targetIndex > -1) {
const item = newData[targetIndex];
const updatedSections = item.attributes.sections.filter(
(s) => s.key !== sectionKey
);
const updatedData = {
...item,
attributes: {
...item.attributes,
sections: updatedSections,
},
};
// 修改更新数据库的调用方式
await supabaseService.update(
"resources",
{ id: record.id },
{ attributes: updatedData.attributes }
);
newData[targetIndex] = updatedData;
setData(newData);
message.success("删除成功");
}
} catch (error) {
console.error("删除失败:", error);
message.error("删除失败");
}
};
const handleDeleteItem = async (record, sectionKey, itemIndex) => {
try {
const newData = [...data];
const targetIndex = newData.findIndex((item) => item.id === record.id);
if (targetIndex > -1) {
const item = newData[targetIndex];
const sections = [...item.attributes.sections];
const sectionIndex = sections.findIndex((s) => s.key === sectionKey);
if (sectionIndex > -1) {
sections[sectionIndex].items = sections[sectionIndex].items.filter(
(_, idx) => idx !== itemIndex
);
if (sections[sectionIndex].items.length === 0) {
sections.splice(sectionIndex, 1);
}
const updatedData = {
...item,
attributes: {
...item.attributes,
sections,
},
};
await supabaseService.update(
"resources",
{ id: record.id },
{ attributes: updatedData.attributes }
);
newData[targetIndex] = updatedData;
setData(newData);
message.success("删除成功");
}
}
} catch (error) {
console.error("删除失败:", error);
message.error("删除失败");
}
};
// 子表格列定义
const expandedRowRender = (record) => {
return (
<div className="bg-gray-50 p-4 rounded-lg">
{record.attributes.sections.map((section) => (
<div key={section.key} className="mb-6 bg-white rounded-lg shadow-sm p-4">
<div className="flex items-center justify-between mb-3 border-b pb-2">
<h3 className="text-lg font-medium text-gray-800">{section.sectionName}</h3>
<Popconfirm
title="确定要删除这个模块吗?"
onConfirm={() => handleDeleteSection(record, section.key)}
>
<DeleteOutlined className="text-red-500 cursor-pointer hover:text-red-600" />
</Popconfirm>
</div>
<Table
dataSource={section.items}
pagination={false}
size="small"
className="nested-items-table border-t border-gray-100"
columns={[
{
title: "名称",
dataIndex: "name",
key: "name",
width: "15%",
render: (text, item, index) => {
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
return isEditing ? (
<Input
defaultValue={text}
onChange={(e) => {
setEditingItem((prev) => ({
...prev || item,
name: e.target.value
}));
}}
/>
) : (
<span>{text}</span>
);
},
},
{
title: "单位",
dataIndex: "unit",
key: "unit",
width: "10%",
render: (text, item, index) => {
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
return isEditing ? (
<Input
defaultValue={text}
onChange={(e) => {
setEditingItem((prev) => ({
...prev || item,
unit: e.target.value
}));
}}
/>
) : (
<span>{text}</span>
);
},
},
{
title: "单价",
dataIndex: "price",
key: "price",
width: "10%",
render: (text, item, index) => {
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
return isEditing ? (
<InputNumber
defaultValue={text}
onChange={(value) => {
setEditingItem((prev) => ({
...prev || item,
price: value
}));
}}
/>
) : (
<span>{text}</span>
);
},
},
{
title: "数量",
dataIndex: "quantity",
key: "quantity",
width: "10%",
render: (text, item, index) => {
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
return isEditing ? (
<InputNumber
defaultValue={text}
onChange={(value) => {
setEditingItem((prev) => ({
...prev || item,
quantity: value
}));
}}
/>
) : (
<span>{text}</span>
);
},
},
{
title: "描述",
dataIndex: "description",
key: "description",
render: (text, item, index) => {
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
return isEditing ? (
<Input
defaultValue={text}
onChange={(e) => {
setEditingItem((prev) => ({
...prev || item,
description: e.target.value
}));
}}
/>
) : (
<span>{text}</span>
);
},
},
{
title: "操作",
key: "action",
width: 150,
render: (_, item, index) => {
const isEditing = editingKey === `${record.id}-${section.key}-${index}`;
return (
<Space>
{isEditing ? (
<>
<SaveOutlined
className="text-green-600 cursor-pointer"
onClick={() => handleSave(record, section.key, index)}
/>
<CloseOutlined
className="text-gray-600 cursor-pointer"
onClick={() => {
setEditingKey("");
setEditingItem(null);
}}
/>
</>
) : (
<>
<EditOutlined
className="text-blue-600 cursor-pointer"
onClick={() => {
setEditingKey(`${record.id}-${section.key}-${index}`);
setEditingItem(item); // 初始化编辑项
}}
/>
<Popconfirm
title="确定要删除这一项吗?"
onConfirm={() => handleDeleteItem(record, section.key, index)}
>
<DeleteOutlined className="text-red-500 cursor-pointer" />
</Popconfirm>
</>
)}
</Space>
);
},
},
]}
/>
</div>
))}
</div>
);
};
const handleNavigateToTemplate = (type, id, mode = "isView") => {
const basePath = TEMPLATE_TYPES[type]?.path;
if (!basePath) return;
const path = id ? `${basePath}/${id}` : basePath;
if (mode === "create") {
navigate(`${basePath}`);
} else {
navigate(`${basePath}/${id}?type=${type}&${mode}=true`);
}
};
// 处理模板类型选择
const handleTemplateSelect = (type) => {
handleNavigateToTemplate(type, null, "create");
setIsModalOpen(false);
};
// 表格列定义
const columns = [
{
title: "模板名称",
dataIndex: ["attributes", "templateName"],
key: "templateName",
className: "min-w-[200px]",
},
{
title: "模板类型",
dataIndex: ["attributes", "template_type"],
key: "template_type",
filters: Object.entries(TEMPLATE_TYPES).map(([key, value]) => ({
text: value.label,
value: key,
})),
onFilter: (value, record) => record.attributes.template_type === value,
render: (type) => {
const typeConfig = TEMPLATE_TYPES[type];
return typeConfig ? (
<Tag
icon={typeConfig.icon}
color={typeConfig.color}
className="px-2 py-1"
>
{typeConfig.label}
</Tag>
) : null;
},
},
{
title: "分类",
dataIndex: ["attributes", "category"],
key: "category",
render: (categories) => {
if (!categories || !Array.isArray(categories)) return null;
return (
<Space size={[0, 8]} wrap>
{categories.map((category) => (
<Tag key={category.id} color="default" className="px-2 py-1">
{category.name}
</Tag>
))}
</Space>
);
},
},
{
title: "描述",
dataIndex: ["attributes", "description"],
key: "description",
className: "min-w-[100px]",
ellipsis: true,
},
{
title: "创建时间",
dataIndex: "created_at",
key: "created_at",
render: (text) => new Date(text).toLocaleString(),
sorter: (a, b) => new Date(a.created_at) - new Date(b.created_at),
},
{
title: "操作",
key: "action",
fixed: "right",
width: 220,
render: (_, record) => (
<Space>
<Button
type="link"
onClick={() =>
handleNavigateToTemplate(
record.attributes.template_type,
record.id,
"isView"
)
}
>
查看
</Button>
<Button
type="link"
onClick={() =>
handleNavigateToTemplate(
record.attributes.template_type,
record.id,
"edit"
)
}
>
编辑
</Button>
<Popconfirm
title="删除确认"
description="确定要删除这个服务模板吗?此操作不可恢复。"
onConfirm={() => handleDeleteService(record.id)}
okText="确定"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button type="link" danger>
删除
</Button>
</Popconfirm>
</Space>
),
},
];
// 表格顶部的操作栏
const TableHeader = () => (
<div className="flex justify-between items-center mb-4">
<Space wrap>
{Object.entries(TEMPLATE_TYPES).map(([key, value]) => (
<Button
key={key}
type={selectedType === key ? "primary" : "default"}
icon={value.icon}
onClick={() => setSelectedType(selectedType === key ? null : key)}
className={`border-${value.color}-500`}
>
{value.label}
<Badge
count={data.filter((item) => item.attributes.type === key).length}
className={`ml-2 bg-${value.color}-100 text-${value.color}-600`}
/>
</Button>
))}
{selectedType && (
<Button onClick={() => setSelectedType(null)}>显示全部</Button>
)}
</Space>
<Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsModalOpen(true)}
className="bg-blue-500 hover:bg-blue-600"
>
新建模板
</Button>
<Divider type="vertical" />
</Space>
</div>
);
return (
<Card className="shadow-lg rounded-lg">
<TableHeader />
<Table
scroll={{x:true}}
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
expandable={{
expandedRowRender,
rowExpandable: (record) => record.attributes.sections?.length > 0,
}}
pagination={{
pageSize: 10,
showTotal: (total) => `${total}`,
}}
className="bg-white rounded-lg"
/>
{/* 添加模板类型选择弹窗 */}
<TemplateTypeModal
open={isModalOpen}
onClose={() => setIsModalOpen(false)}
onSelect={handleTemplateSelect}
/>
</Card>
);
};
export default ServicePage;