603 lines
18 KiB
JavaScript
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;
|